package ui;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.antlr.runtime.ANTLRInputStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.TokenRewriteStream;

import script.grammar.FilterTranslator;
import ui.grammar.LocCmdLexer;
import ui.grammar.LocCmdParser;

import data.Cell;
import data.Cluster;
import data.Group;
import data.analysis.Analyzer;
import data.analysis.Analyzer.Filter;
import data.analysis.Analyzer.Results;
import data.analysis.Statistic;
import data.grammar.LocDataInputStream;

import event.DebugLogger;
import event.ErrorLogger;
import event.EventHandler;

/**
 * Command-line user interface for LCAS.
 * @author Charles
 *
 */
@Version("1.1.0")
public class UserInterface {
	private static final String NEW_LINE = "\r\n";
	private static final String DEFAULT_PRINTOUT_SUFFIX = "_PRINTOUT";
	private static final String DEFAULT_PRINTOUT_EXT = ".txt";
	
	private FilterTranslator filterTrans;

	private DebugLogger db;
	private ErrorLogger err;

	private ArrayList<File> inputs;
	private ArrayList<Group> groups;
	private byte[] ruleScriptSource = null;
	private byte[] statScriptSource = null;
	private byte[] cellFilterScriptSource = null;
	private Filter<Cluster> rs = null;
	private List<Statistic> stats = null;
	private Analyzer.Filter<Cell> cf = null;
	private List<Cell> filtered = null;

	// options
	private boolean ignoreBadData = false;
	private boolean verbose = false;
	private boolean silent = false;
	private boolean print = false;
	private boolean table = false;
	private File printTo = null;
	
	private boolean help = false;
	private Map<String,String> helpTable;
	
	/**
	 * Creates a new <code>UserInterface</code> object. Default {@link DebugLogger} and 
	 * {@link ErrorLogger} instances are used. The default <code>DebugLogger</code> is 
	 * switched off and prints to <code>System.out</code>. The default <code>ErrorLogger</code> 
	 * prints to <code>System.err</code>.
	 */
	public UserInterface() {
		this(new DebugLogger(System.out, false), new ErrorLogger(System.err));
	}
	
	/**
	 * Creates a new <code>UserInterface</code> object with the specified <code>DebugLogger</code> 
	 * and <code>ErrorLogger</code>.
	 * @param d the {@link DebugLogger} to be used.
	 * @param e the {@link ErrorLogger} to be used.
	 */
	public UserInterface(DebugLogger d, ErrorLogger e) {
		this.filterTrans = new FilterTranslator(d, e);
		this.db = d;
		this.err = e;
		this.inputs = new ArrayList<File>();
		this.groups = new ArrayList<Group>();
		this.stats = getDefaultStats();
		
		initHelpTable();
	}
	
	public String getName() {
		return "lant v" + this.getClass().getAnnotation(Version.class).value();
	}

	public DebugLogger getDebugLogger() {
		return db;
	}

	public ErrorLogger getErrorLogger() {
		return err;
	}

	public void setDebugMode(boolean b) {
		db.setActive(b);
	}
	
	public void help() {
		help("");
	}
	
	/**
	 * Prints the appropriate help string to <code>System.out</code>.
	 * @param s the argument for the help command, or an empty string if there is 
	 * no argument.
	 */
	public void help(String s) {
		help = true;
		System.out.println(buildHelpString(s));
	}

	protected Group parseInputFile(File f) {
		FileInputStream fin;
		Group g = null;
		try {
			fin = new FileInputStream(f);

			try {
				LocDataInputStream in = new LocDataInputStream(fin, err, db);
				g = in.readGroup(f.getName());
			} catch (IOException e) {
				try {
					err.logErrorMsg("Error: Parse of data file failed: " + f.getCanonicalPath());
				} catch (IOException e1) {}
			}
		} catch (FileNotFoundException e) {
			try {
				err.logErrorMsg("Error: Could not open file: " + f.getCanonicalPath());
			} catch (IOException e1) {}
		}
		return g;
	}
	
	public void loadInputFile(File f) {
		if (f.isDirectory()) {
			try {
				db.logDebugMsg("Loading input directory... " + f.getCanonicalPath());
			} catch (IOException e) {}
			File[] files = f.listFiles();
			for (int i = 0; i < files.length; ++i) {
				if (!files[i].isDirectory()) {
					try {
						db.logDebugMsg("Loading input file... " + files[i].getCanonicalPath());
					} catch (IOException e) {}
					inputs.add(files[i]);
				}
			}
		} else {
			try {
				db.logDebugMsg("Loading input file... " + f.getCanonicalPath());
			} catch (IOException e) {}
			inputs.add(f);
		}
	}
	
	/**
	 * Parses the data input files 
	 */
	public void parseInput() {
		db.logDebugMsg("Parsing input files...");
		for (File f : inputs) {
			Group g = parseInputFile(f);
			if (g != null) {
				groups.add(g);
			} else if (ignoreBadData) {
				try {
					db.logDebugMsg("Discarding bad data file: " + f.getCanonicalPath());
				} catch (IOException e) {}
			} else {
				throw new RuntimeException();
			}
		}
		db.logDebugMsg("...done.");
	}
	
	/**
	 * Attempts to parse and compile a foci rule script from source code for use in 
	 * subsequent analyses.
	 * @param in an {@link InputStream} initialized to the beginning of the script source 
	 * code.
	 * @throws RuntimeException if the source code cannot be read or compiled.
	 */
	public void setFociScript(InputStream in) {
		try {
			ruleScriptSource = pullBytes(in);
			
			Filter<Cluster> script = filterTrans.instantiateFilter(in, Cluster.class, "FociScriptImpl");
			if (script == null) {
				err.logErrorMsg("Error: Could not compile foci script.");
				throw new RuntimeException();
			}
			rs = script;
		} catch (IOException e) {
			this.err.logErrorMsg("Error: Could not read foci script.");
			throw new RuntimeException();
		}
	}
	
	/**
	 * Attempts to parse statistic definitions from source code for use in subsequent 
	 * analyses.
	 * @param in an {@link InputStream} initialized to the beginning of the definition 
	 * source code.
	 * @param replace <code>true</code> if the parsed definitions should replace the default 
	 * statistics defined by the program.
	 */
	public void setStatScript(InputStream in, boolean replace) {
		data.analysis.grammar.StatTranslator t = new data.analysis.grammar.StatTranslator(filterTrans, db, err);
		try {
			statScriptSource = pullBytes(in);
			
			List<Statistic> list = t.buildStats(in);
			if (list != null) {
				if (replace) {
					stats = list;
				} else {
					stats.addAll(list);
				}
			} else {
				this.err.logErrorMsg("Error: Could not compile statistics script. Reverting to default statistics.");
			}
		} catch (IOException e) {
			this.err.logErrorMsg("Error: Could not read statistics script. Reverting to default statistics.");
		}
	}
	
	/**
	 * Attempts to parse and compile a cell filter script from source code for use in 
	 * subsequent analyses.
	 * @param in an {@link InputStream} initialized to the beginning of the script source 
	 * code.
	 * @throws RuntimeException if the source code cannot be read or compiled.
	 */
	public void setCellFilter(InputStream in) {
		try {
			cellFilterScriptSource = pullBytes(in);
			
			Filter<Cell> script = filterTrans.instantiateFilter(in, Cell.class, "CellFilterImpl");
			if (script == null) {
				err.logErrorMsg("Error: Could not compile cell filter script.");
				throw new RuntimeException();
			}
			cf = script;
		} catch (IOException e) {
			this.err.logErrorMsg("Error: Could not read cell filter script.");
			throw new RuntimeException();
		}
	}
	
	/**
	 * Returns a list of the default statistics used by the program.
	 * @return a {@link List} of the default statistics used by the program.
	 */
	public List<Statistic> getDefaultStats() {
		List<Statistic> l = new ArrayList<Statistic>();
		Collections.addAll(l, 
				new Statistic("No. of cells") {
					@Override
					public Object calculate(Results re) {
						return re.countCells();
					}
				}, new Statistic("No. of focal cells") {
					@Override
					public Object calculate(Results re) {
						return re.countFocalCells();
					}
				}, new Statistic("No. of nonfocal cells") {
					@Override
					public Object calculate(Results re) {
						return re.countNonFocalCells();
					}
				},
				new Statistic("Fraction of focal cells") {
					@Override
					public Object calculate(Results re) {
						return re.fractionFocalCells();
					}
				}, new Statistic("No. of cytoplasmic focal cells") {
					@Override
					public Object calculate(Results re) {
						return re.countCytoplasmicFocalCells();
					}
				}, new Statistic("No. of membrane focal cells") {
					@Override
					public Object calculate(Results re) {
						return re.countMembraneFocalCells();
					}
				}
		);
		return l;
	}
	
	/**
	 * Reads all available bytes from an <code>InputStream</code> and dumps them in a 
	 * <code>byte</code> array then resets the stream to the position it was in 
	 * before this method was called.
	 * @param in the {@link InputStream} to read.
	 * @return an array of <code>bytes</code> containing the contents of the 
	 * input stream.
	 * @throws IOException if an error occurred while reading from the stream or 
	 * if the stream could not be reset.
	 */
	protected byte[] pullBytes(InputStream in) throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		int read;
		in.mark(0);
		while ((read = in.read()) != -1) {
			baos.write(read);
		}
		in.reset();
		return baos.toByteArray();
	}
	
	/**
	 * Generates a new <code>File</code> object with the specified path.
	 * @param s the path of the file.
	 * @return a new {@link File} object with the specified path.
	 */
	public File retrieveFile(String s) {
		try {
			return new File(s);
		} catch (NullPointerException e) {
			this.err.logErrorMsg("Error: Null file path detected.");
			throw new RuntimeException();
		}
	}
	
	/**
	 * Constructs an {@link InputStream} from a <code>String</code> containing a script source. The 
	 * stream is initialized to the beginning of the string.
	 * @param s the string to read.
	 * @return an InputStream over the string, initialized to the beginning of the string.
	 */
	public InputStream buildInputStream(String s) {
		this.db.logDebugMsg("Initializing input stream for script from string: " + s);
		return new ByteArrayInputStream(s.getBytes());
	}
	
	/**
	 * Constructs an {@link InputStream} from a script file. The stream is initialized to the 
	 * beginning of the file.
	 * @param f the file to read.
	 * @return an InputStream over the file, initialized to the beginning of the file, or <code>null</code>
	 * if the stream cannot be created.
	 */
	public InputStream buildInputStream(File f) {
		try {
			this.db.logDebugMsg("Initializing input stream for script from file: " + f.getCanonicalPath());
		} catch (IOException e2) {
		}
		try {
			InputStream in = new FileInputStream(f);
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			int read;
			while ((read = in.read()) != -1) {
				baos.write(read);
			}
			return new ByteArrayInputStream(baos.toByteArray());
		} catch (FileNotFoundException e) {
			try {
				this.err.logErrorMsg("Error: Could not open file: " + f.getCanonicalPath());
			} catch (IOException e1) {
				this.err.logErrorMsg("Error: Could not open file.");
			}
			throw new RuntimeException();
		} catch (IOException e) {
			this.err.logErrorMsg("Error: Could not initialize input stream for script.");
			throw new RuntimeException();
		}
	}
	
	public void setIgnoreBadData() {
		ignoreBadData = true;
	}

	public void setVerbose() {
		verbose = true;
	}

	public void setPrint() {
		print = true;
	}

	public void setPrint(File f) {
		setPrint();
		printTo = f;
	}
	
	public void setSilent() {
		silent = true;
	}
	
	public void setTable() {
		table = true;
	}
	
	/**
	 * Parses the command-line arguments. When this method returns, all relevant data 
	 * files will be parsed and information collected and all the appropriate program 
	 * options will be set.
	 * @param in the input stream from which to read the command-line arguments.
	 * @throws RuntimeException if the arguments are malformed or if the program cannot 
	 * read from the input stream.
	 */
	protected void readArgs(InputStream in) {
		try {
			CharStream input = new ANTLRInputStream(in);
			LocCmdLexer lex = new LocCmdLexer(input);
			final TokenRewriteStream tokens = new TokenRewriteStream(lex);
			LocCmdParser p = new LocCmdParser(tokens, this);
			try {
				p.args();
			} catch (RecognitionException e) {
				e.printStackTrace();
			}

			if (p.getNumberOfSyntaxErrors() > 0) {
				this.err.logErrorMsg("Error: Malformed command-line arguments. Type 'java -jar <jarfile> --help' for usage information.");
				throw new RuntimeException();
			}
		} catch (IOException e) {
			this.err.logErrorMsg("Fatal error: could not initialize console input stream.");
			throw new RuntimeException();
		}
	}
	
	/**
	 * Finds an open file name for a group in the selected directory and returns 
	 * a new {@link File} object with that name.
	 * @param g the group for which to create a new file.
	 * @param directory the directory in which the file should be created.
	 * @return a new {@link File} object in the specified directory with the appropriate name.
	 * @throws IllegalArgumentException if the specified directory is a valid directory.
	 */
	protected File findDefaultFile(Group g, File directory) {
		if (directory != null && !directory.isDirectory()) {
			throw new IllegalArgumentException();
		}
		
		File f;
		String name;
		if (g == null) {
			Calendar currentTime = new GregorianCalendar();
			currentTime.setTime(new Date());
			int month = currentTime.get(Calendar.MONTH) + 1;
			int day = currentTime.get(Calendar.DATE);
			name = currentTime.get(Calendar.YEAR) + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day;
		} else {
			name = g.getId();
		}
		
		int i = 0;
		do {
			if (i == 0) {
				f = new File(directory, name + DEFAULT_PRINTOUT_SUFFIX + DEFAULT_PRINTOUT_EXT);
			} else {
				f = new File(directory, name + DEFAULT_PRINTOUT_SUFFIX + " (" + i + ")" + DEFAULT_PRINTOUT_EXT);
			}
			++i;
		} while (f.exists());
		return f;
	}
	
	protected File findDefaultFile(File directory) {
		return findDefaultFile(null, directory);
	}
	
	protected File findDefaultFile(Group g) {
		return findDefaultFile(g, null);
	}
	
	protected File findDefaultFile() {
		return findDefaultFile(null, null);
	}
	
	/**
	 * Runs analysis on a group.
	 * @param g the {@link Group} to be analyzed.
	 * @return the results of the analysis.
	 */
	protected Analyzer.Results analyzeGroup(Group g) {
		db.logDebugMsg("...analyzing Group " + g.getId() + " ...");
		Analyzer a = new Analyzer(g);
		a.runAnalysis(rs);
		if (cf != null) {
			a.applyCellFilter(cf);
		}
		Analyzer.Results re = a.getResults();
		db.logDebugMsg("Analysis of " + g.getId() + " took " + (re.getTime() / 1000.) + " seconds.");
		return re;
	}
	
	private void setupPrint(Group g, List<Writer> writeTo) {
		try {
			if (printTo != null && printTo.isDirectory()) { // user specified print to directory
				printTo = findDefaultFile(g, printTo);
			} else {
				printTo = findDefaultFile(g);
			}
			writeTo.add(new FileWriter(printTo));
		} catch (IOException e) {
			this.err.logErrorMsg("Error: could not open output file. Skipping print to file.");
			print = false;
		}
	}
	
	private void writeOutput(String s, List<Writer> writeTo) {
		ForkedWriter wr = new ForkedWriter(writeTo);
		try {
			wr.write(s);
			wr.flush();
			if (print) {
				try {
					System.out.println("Output was printed to: " + printTo.getCanonicalPath());
				} catch (IOException e) {}
			}
		} catch (IOException e) {
			this.err.logErrorMsg("Error: A problem was encountered when writing the output to one or more of the "
					+ "output destinations.");
		}
	}
	
	/**
	 * Runs analysis on any groups stored within program memory and prints the results to 
	 * the appropriate targets.
	 */
	protected void runAnalysis() {
		db.logDebugMsg("Analysis started...");
		List<Writer> writeTo = new ArrayList<Writer>();
		if (!silent) { // print to stdout as well
			writeTo.add(new PrintWriter(System.out));
		}
		
		ArrayList<Analyzer.Results> results = new ArrayList<Analyzer.Results>(groups.size());
		for (Group g : groups) {
			results.add(analyzeGroup(g));
		}
		db.logDebugMsg("...done.");
		
		if (table) { // print results formatted as a table/spreadsheet
			if (print) {
				setupPrint(null, writeTo);
			}
			String s = bufferTableOutput(results, stats);	
			writeOutput(s, writeTo);
		} else { // print results formatted as a list
			for (Analyzer.Results re : results) {
				if (print) { // print results to file
					setupPrint(re.getGroup(), writeTo);
				}
				String s = bufferOutput(re, stats);
				writeOutput(s, writeTo);
			}
		}
	}
	
	/**
	 * Builds and returns the header of the output for an analysis group. The header 
	 * contains the group ID and the timestamp of the analysis. If verbose mode is 
	 * enabled, it also contains the user-defined foci rule and cell filter scripts.
	 * @param g the {@link Group} for which to build a header.
	 * @return the header of the output of the analysis group.
	 */
	private String buildHeader(Group g) {
		StringBuilder buf = new StringBuilder();
		Calendar currentTime = new GregorianCalendar();
		currentTime.setTime(new Date());
		
		buf.append("OUTPUT FROM " + getName() + NEW_LINE + NEW_LINE);
		
		if (g == null) {
			buf.append("Analysis of multiple groups:" + NEW_LINE);
		} else {
			buf.append("Analysis of group " + g.getId() + ":" + NEW_LINE);
		}
		
		int hr = currentTime.get(Calendar.HOUR_OF_DAY);
		int min = currentTime.get(Calendar.MINUTE);
		int day = currentTime.get(Calendar.DATE);
		int month = currentTime.get(Calendar.MONTH) + 1;
		int year = currentTime.get(Calendar.YEAR);
		buf.append("[" + (hr < 10 ? "0" : "") + hr + ":" + (min < 10 ? "0" : "") + min + " "
				+ (day < 10 ? "0" : "") + day + "/" + (month < 10 ? "0" : "") + month + "/" + year + "]" + NEW_LINE + NEW_LINE);
		if (verbose) {
			buf.append("Foci rule script: " + NEW_LINE + NEW_LINE);
			buf.append(">>" + NEW_LINE);
			buf.append(new String(ruleScriptSource) + NEW_LINE);
			buf.append(">>" + NEW_LINE + NEW_LINE);
			
			if (cf != null) {
				buf.append("Cell filter script: " + NEW_LINE + NEW_LINE);
				buf.append(">>" + NEW_LINE);
				buf.append(new String(cellFilterScriptSource) + NEW_LINE);
				buf.append(">>" + NEW_LINE + NEW_LINE);
			}
		}
		buf.append("******" + NEW_LINE + NEW_LINE);
		return buf.toString();
	}
	
	private String buildHeader() {
		return buildHeader(null);
	}
	
	/**
	 * Buffers the output of the analysis formatted as a list and returns the 
	 * buffer as a <code>String</code>.
	 * @param re the results of the analysis.
	 * @param stats a list of statistics to include in the output.
	 * @return the output of the analysis.
	 */
	protected String bufferOutput(Analyzer.Results re, List<Statistic> stats) {
		StringBuilder buf = new StringBuilder();
		
		buf.append(buildHeader(re.getGroup()));
		for (Statistic s : stats) {
			buf.append(s.getName() + ": ");
			long start = System.currentTimeMillis();
			buf.append(s.calculate(re));
			if (verbose) {
				buf.append(" [" + ((System.currentTimeMillis() - start) / 1000.0) + "s]");
			}
			buf.append(NEW_LINE);
		}
		buf.append(NEW_LINE);
		return buf.toString();
	}
	
	/**
	 * Buffers the output of the analysis formatted as a table and returns the 
	 * buffer as a <code>String</code>.
	 * @param results the results of the analysis.
	 * @param stats a list of statistics to include in the output.
	 * @return the output of the analysis.
	 */
	protected String bufferTableOutput(List<Analyzer.Results> results, List<Statistic> stats) {
		String[][] table = new String[stats.size() + 1][results.size() + 1];
		int j = 0;
		for (Analyzer.Results re : results) {
			int i = 0;
			table[0][j+1] = re.getGroup().getId();
			for (Statistic s : stats) {
				if (j == 0) {
					table[i+1][j] = s.getName();
				}
				table[i+1][j+1] = "" + s.calculate(re);
				++i;
			}
			++j;
		}
		
		StringBuilder buf = new StringBuilder();
		buf.append(buildHeader());
		for (int m = 0; m < table.length; ++m) {
			for (int n = 0; n < table[m].length; ++n) {
				buf.append(table[m][n] == null ? "" : table[m][n]);
				if (n < table[m].length - 1) {
					buf.append("\t");
				}
			}
			buf.append("\r\n");
		}
		return buf.toString();
	}
	
	private void initHelpTable() {
		helpTable = new HashMap<String,String>();
		StringBuilder buf = new StringBuilder();
		
		// default
		buf.append("\n" + getName() + "\n");
		buf.append("Author: Charles Wang\n\n");
		buf.append("java -jar <jarfile> [<--debug> (OPTIONAL)] -i <input> -f <foci_script> [-s <stat_script> (OPTIONAL)] [OPTIONS]\n\n");
		buf.append("OPTIONS:\n");
		buf.append("--verbose\n");
		buf.append("--silent\n");
		buf.append("--filter <cell filter script>\n");
		buf.append("--print [<directory> (OPTIONAL)]\n");
		buf.append("--table\n\n");
		buf.append("For detailed information on an option or field, use '--help <OPTION>'\n\te.g. '--help verbose' or '--help input'.");
		helpTable.put("", buf.toString());
		
		// debug
		buf.delete(0, buf.length());
		buf.append("\nMODE '--debug':\n\n");
		buf.append("Switches the program to debug mode.\n\nMore information on program behavior and execution status will be printed to the console.");
		helpTable.put("debug", buf.toString());
		
		// verbose
		buf.delete(0, buf.length());
		buf.append("\nOPTION '--verbose':\n\n");
		buf.append("Output will include additional data.");
		helpTable.put("verbose", buf.toString());
		
		// silent
		buf.delete(0, buf.length());
		buf.append("\nOPTION '--silent':\n\n");
		buf.append("No output will be printed to the console.");
		helpTable.put("silent", buf.toString());
		
		// filter
		buf.delete(0, buf.length());
		buf.append("\nOPTION '--filter <filter_script>':\n\n");
		buf.append("Filters out cells after the analysis is complete.\n\nFiltered cells will not show up in the reported statistics.\n");
		buf.append("The script is defined in the same manner as <foci_script>. For more information use '--help foci_script'.");
		helpTable.put("filter", buf.toString());
		
		// print
		buf.delete(0, buf.length());
		buf.append("\nOPTION '--print [<directory> (OPTIONAL)]':\n\n");
		buf.append("Prints the output to a .txt file.\n\nIf more than one input file was analyzed, one .txt file will be created for each input (unless the '--table' option is selected).\n");
		buf.append("If a directory isn't specified, the .txt file will be created in the same directory as the .jar file from which the program is run.\n");
		helpTable.put("print", buf.toString());
		
		// table
		buf.delete(0, buf.length());
		buf.append("\nOPTION '--table':\n\n");
		buf.append("Outputs the statistic data as a table instead of a list.");
		helpTable.put("table", buf.toString());
		
		// input
		buf.delete(0, buf.length());
		buf.append("\nFIELD '<input>':\n");
		buf.append("\tOption 1: FILENAME\n\tOption 2: FILENAME FILENAME ...\n\tOption 3: DIRECTORY\n\n");
		buf.append("Specify the input containing the data to be analyzed.\n\nCan be a single file, a space-separated list of files, or a directory.");
		helpTable.put("input", buf.toString());
		
		// foci_script
		buf.delete(0, buf.length());
		buf.append("\nFIELD '<foci_script>':\n");
		buf.append("\tOption 1: FILENAME\n\tOption 2: { INLINE_SCRIPT }\n\n");
		buf.append("Specify the rule used to mark clusters as foci.\n\nCan be a file containing the script or an inline script.");
		helpTable.put("foci_script", buf.toString());
		
		// stat_script
		buf.delete(0, buf.length());
		buf.append("\nFIELD '<stat_script>':\n");
		buf.append("\tOption 1: [-r (OPTIONAL)] FILENAME\n\tOption 2: [-r (OPTIONAL)] { INLINE_SCRIPT }\n\n");
		buf.append("Specify the statistics to be calculated and printed in the output.\n\nCan be a file containing the definitions or an inline script.\n");
		buf.append("If the '-r' flag is set, the program will replace the default set of statistics with the user-defined ones. Otherwise it calculates the user-defined statistics in addition to the default.");
		helpTable.put("stat_script", buf.toString());
	}
	
	private String buildHelpString(String s) {
		String display = helpTable.get(s);
		return display == null ? "Command not recognized, use '--help' to view command list." : display;
	}
	
	/**
	 * Runs the program with the given command-line arguments.
	 * @param in the input stream from which to read the command-line arguments.
	 */
	public void run(InputStream in) {
		readArgs(in);
		if (!help) {
			runAnalysis();
		}
	}

	public static void main(String[] args) {
		DebugLogger d = new DebugLogger(System.out, false);
		ErrorLogger e = new ErrorLogger(System.err);
		UserInterface ui = new UserInterface(d, e);
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		try {
			for (int i = 0; i < args.length; ++i) {
				baos.write(args[i].getBytes());
				baos.write(new byte[]{'\r', '\n'});
			}
			ui.run(new ByteArrayInputStream(baos.toByteArray()));
		} catch (IOException ex) {
			e.logErrorMsg("Fatal error: could not initialize console input stream.");
		} catch (RuntimeException ex) {
			e.logErrorMsg("An error has occured. The program is shutting down...");
			e.logErrorMsg(ex);
		} finally {
			EventHandler.getEventHandler().terminate();
		}
	}
}
